/* * HNTB Project Portal — Hostinger subdomain gate (PHP front controller). * * Drop this + .htaccess + gate-config.php into a subdomain's docroot. The * PORTAL provides these files; project teams never write auth code. Every * request to the subdomain runs through here FIRST: it validates the * .hntbprojectportal.com Cognito session cookie server-side and only then * serves the file. No valid session -> bounce to the portal login. * * Self-contained: no Composer, no libraries. Verifies RS256 with OpenSSL. */ declare(strict_types=1); // ---- Cognito (public values; identical for every subdomain) ---- const REGION = 'us-east-2'; const POOL_ID = 'us-east-2_q3otFhTB7'; const CLIENT_ID = '7vsa9cqju2qiqsg4t3fjjlc1rb'; const COOKIE = 'hntb_session'; const PIN_COOKIE = 'hntb_pin'; // signed cookie set after a correct shared PIN const LOGIN_URL = 'https://hntbprojectportal.com/'; const FULL_ACCESS = ['portal_admin', 'hntb_staff']; // reach every subdomain $ISSUER = 'https://cognito-idp.' . REGION . '.amazonaws.com/' . POOL_ID; $JWKS_URI = $ISSUER . '/.well-known/jwks.json'; // Per-subdomain settings (the only file you edit when creating a subdomain). $cfg = is_file(__DIR__ . '/gate-config.php') ? (require __DIR__ . '/gate-config.php') : []; $REQUIRED_GROUPS = $cfg['required_groups'] ?? []; $PUBLIC_PATHS = $cfg['public_paths'] ?? []; $DEFAULT_DOC = $cfg['default_doc'] ?? 'index.html'; // file served at "/" // ------------------------------------------------------------ helpers -- function b64url(string $s): string { return base64_decode(strtr($s, '-_', '+/') . str_repeat('=', (4 - strlen($s) % 4) % 4)); } function redirect_login(): void { global $cfg; // Branded interstitial instead of an instant cross-host 302 to the login. // A user-initiated "Sign in" click looks legitimate to corporate web filters; // an automatic redirect to a credential page on another host is the exact // pattern those filters classify as phishing (which got our subdomains blocked). $host = $_SERVER['HTTP_HOST'] ?? ''; $target = 'https://' . $host . ($_SERVER['REQUEST_URI'] ?? '/'); // PIN-first subdomains: partner PIN entry is the primary screen and the // HNTB SSO login is the smaller secondary link (rendered by pin_page, // which also sets the pp_return cookie itself). if (pin_enabled($cfg) && !empty($cfg['pin_first'])) pin_page($cfg, $host); // Stash the return target in a cookie on the parent domain instead of putting a // cross-host redirect URL in the login link. Google Safe Browsing flags a login // URL carrying ?redirect= as a phishing-style redirect; a plain // ?login=1 link + this cookie removes that signal. The apex reads pp_return after login. setcookie('pp_return', $target, [ 'expires' => time() + 600, 'path' => '/', 'domain' => '.hntbprojectportal.com', 'secure' => true, 'httponly' => false, 'samesite' => 'Lax', ]); $href = htmlspecialchars(LOGIN_URL . '?login=1', ENT_QUOTES); $pinLink = (isset($cfg['access_pin']) && $cfg['access_pin'] !== '' && $cfg['access_pin'] !== 'CHANGE_ME') ? '
Partner agency? Enter access PIN instead.
' : ''; header('Cache-Control: no-store'); header('Content-Type: text/html; charset=utf-8'); http_response_code(200); echo << HNTB Project Portal — Sign in
Project Portal · Secure Workspace

Sign in to continue

This is a secure HNTB project workspace. Sign in with your work email to view this project.

Sign in to continue $pinLink
HNTB · Digital Project Delivery
HTML; exit; } function deny(string $host): void { http_response_code(403); header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-store'); echo 'No access' . '' . '

No access

Your account isn’t granted access to ' . htmlspecialchars($host) . '. ' . 'Request it from the portal.

'; exit; } // JWKS, cached to temp with a 6h TTL. function get_jwks(string $uri): array { $cache = sys_get_temp_dir() . '/hntb_jwks_' . md5($uri) . '.json'; if (is_file($cache) && (time() - filemtime($cache) < 21600)) { $d = json_decode((string) file_get_contents($cache), true); if (isset($d['keys'])) return $d; } $raw = @file_get_contents($uri); if ($raw === false && function_exists('curl_init')) { $ch = curl_init($uri); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]); $raw = curl_exec($ch); curl_close($ch); } $d = json_decode((string) $raw, true); if (isset($d['keys'])) { @file_put_contents($cache, $raw); return $d; } return ['keys' => []]; } // Build an RSA public key (PEM) from a JWK's modulus/exponent (no libraries). function jwk_to_pem(array $jwk): ?string { if (!isset($jwk['n'], $jwk['e'])) return null; $len = function (int $n): string { if ($n < 0x80) return chr($n); $o = ''; while ($n > 0) { $o = chr($n & 0xff) . $o; $n >>= 8; } return chr(0x80 | strlen($o)) . $o; }; $int = function (string $b) use ($len): string { $b = ltrim($b, "\x00"); if ($b === '') $b = "\x00"; if (ord($b[0]) & 0x80) $b = "\x00" . $b; return "\x02" . $len(strlen($b)) . $b; }; $seq = $int(b64url($jwk['n'])) . $int(b64url($jwk['e'])); $rsa = "\x30" . $len(strlen($seq)) . $seq; $bit = "\x03" . $len(strlen($rsa) + 1) . "\x00" . $rsa; $alg = "\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00"; $spki = "\x30" . $len(strlen($alg . $bit)) . $alg . $bit; return "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($spki), 64, "\n") . "-----END PUBLIC KEY-----\n"; } function verify_token(string $jwt, array $jwks, string $issuer): ?array { $p = explode('.', $jwt); if (count($p) !== 3) return null; $header = json_decode(b64url($p[0]), true); if (!isset($header['kid'])) return null; $jwk = null; foreach ($jwks['keys'] as $k) if (($k['kid'] ?? '') === $header['kid']) { $jwk = $k; break; } if (!$jwk) return null; $pem = jwk_to_pem($jwk); if ($pem === null) return null; if (openssl_verify($p[0] . '.' . $p[1], b64url($p[2]), $pem, OPENSSL_ALGO_SHA256) !== 1) return null; $c = json_decode(b64url($p[1]), true); if (!is_array($c)) return null; if (($c['exp'] ?? 0) < time()) return null; if (($c['iss'] ?? '') !== $issuer) return null; if (($c['aud'] ?? '') !== CLIENT_ID) return null; if (($c['token_use'] ?? '') !== 'id') return null; return $c; } // Serve a static file from this docroot (only reached once authorized). function serve_file(string $defaultDoc = 'index.html'): void { $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $rel = ($uri === '/') ? '/' . $defaultDoc : rawurldecode($uri); $path = realpath(__DIR__ . $rel); if ($path !== false && is_dir($path)) $path = realpath($path . '/index.html'); $base = realpath(__DIR__); if ($path === false || strncmp($path, $base, strlen($base)) !== 0) { http_response_code(404); echo 'Not found'; exit; } if (in_array(basename($path), ['gate.php', 'gate-config.php', '.htaccess', '.hntb-visitors.jsonl'], true)) { http_response_code(404); echo 'Not found'; exit; } $types = [ 'html' => 'text/html; charset=utf-8', 'htm' => 'text/html; charset=utf-8', 'css' => 'text/css', 'js' => 'application/javascript', 'mjs' => 'application/javascript', 'json' => 'application/json', 'map' => 'application/json', 'svg' => 'image/svg+xml', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'webp' => 'image/webp', 'ico' => 'image/x-icon', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'ttf' => 'font/ttf', 'pdf' => 'application/pdf', 'csv' => 'text/csv', 'txt' => 'text/plain', ]; $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); header('Content-Type: ' . ($types[$ext] ?? 'application/octet-stream')); header('Cache-Control: private, no-store'); readfile($path); exit; } // Self-register this subdomain with the portal registry so it auto-appears in // the project list — no central manifest. Fire-and-forget, throttled to once a // day via a temp stamp file, with a short timeout so it never delays the page. function maybe_register(array $cfg, string $host): void { $secret = $cfg['register_secret'] ?? ''; $api = rtrim((string) ($cfg['api_base'] ?? ''), '/'); if ($secret === '' || $secret === 'CHANGE_ME' || $api === '' || !function_exists('curl_init')) return; $stamp = sys_get_temp_dir() . '/hntb_reg_' . md5($host) . '.stamp'; if (is_file($stamp) && (time() - filemtime($stamp) < 86400)) return; @touch($stamp); // stamp first so a slow/failed call doesn't retry every hit $slug = explode('.', $host)[0] ?: $host; $proj = $cfg['project'] ?? []; $payload = json_encode([ 'slug' => $slug, 'host' => $host, 'name' => $proj['name'] ?? '', 'blurb' => $proj['blurb'] ?? '', 'client' => $proj['client'] ?? '', 'lat' => $proj['lat'] ?? null, // for the portal map/globe (optional) 'lng' => $proj['lng'] ?? null, 'required_groups' => $cfg['required_groups'] ?? [], ]); $ch = curl_init($api . '/projects/register'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['content-type: application/json', 'x-portal-secret: ' . $secret], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1, ]); @curl_exec($ch); @curl_close($ch); } // =============== Direct PIN access + visitor logging (add-on) =============== // Alternate to Cognito SSO: a visitor who enters the shared PIN is issued a // signed, expiring cookie. Every authorized visit is appended to a server-side // log so you can see WHO (by IP / reverse-DNS) is using the dashboard. function pin_enabled(array $cfg): bool { return isset($cfg['access_pin']) && $cfg['access_pin'] !== '' && $cfg['access_pin'] !== 'CHANGE_ME'; } function client_ip(): string { // REMOTE_ADDR is the authoritative connecting peer. Forwarded headers are // spoofable, so this is the source of truth; fwd_ip() is only a hint. return (string) ($_SERVER['REMOTE_ADDR'] ?? ''); } function fwd_ip(): string { foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP'] as $h) { if (!empty($_SERVER[$h])) return trim(explode(',', (string) $_SERVER[$h])[0]); } return ''; } function host_only(string $host): string { return preg_replace('/:\d+$/', '', $host); } function visitor_log_path(array $cfg): string { if (!empty($cfg['visitor_log'])) return $cfg['visitor_log']; // Prefer a location OUTSIDE the web docroot; every URL here routes through // gate.php anyway, but keeping the log out of the docroot is belt-and-braces. $out = dirname(__DIR__) . '/.hntb-visitors.jsonl'; if (is_dir(dirname($out)) && is_writable(dirname($out))) return $out; return __DIR__ . '/.hntb-visitors.jsonl'; // fallback (blocked from being served) } function log_visit(array $cfg, string $host, string $method, string $who = ''): void { $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $isLogin = (strpos($method, 'login') !== false); // Log page navigations (or explicit logins); skip assets to keep it readable. $isPage = ($path === '/' || preg_match('/\.html?$/i', $path) || $isLogin); if (!$isPage) return; if (empty($cfg['log_page_views']) && !$isLogin) return; // sign-ins only mode $rec = [ 'ts' => gmdate('c'), 'ip' => client_ip(), 'fwd' => fwd_ip(), 'method' => $method, // pin | sso | pin-login | sso-login 'who' => $who, // email for SSO; '' for PIN 'path' => $path, 'ua' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 300), ]; @file_put_contents(visitor_log_path($cfg), json_encode($rec, JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX); } // --- brute-force throttle (per IP, temp-file counter with a lockout window) --- function pin_throttle_file(): string { return sys_get_temp_dir() . '/hntb_pin_' . md5(client_ip()); } function pin_locked(): bool { $d = @json_decode((string) @file_get_contents(pin_throttle_file()), true); if (!is_array($d)) return false; // 8 failures within 15 min -> locked for the remainder of the window. return ($d['n'] ?? 0) >= 8 && (time() - ($d['t'] ?? 0)) < 900; } function pin_fail(): void { $d = @json_decode((string) @file_get_contents(pin_throttle_file()), true); if (!is_array($d) || (time() - ($d['t'] ?? 0)) > 900) $d = ['n' => 0, 't' => time()]; $d['n'] = ($d['n'] ?? 0) + 1; @file_put_contents(pin_throttle_file(), json_encode($d), LOCK_EX); } function pin_reset(): void { @unlink(pin_throttle_file()); } // --- signed PIN cookie (HMAC-SHA256; rotating the PIN invalidates old cookies) --- function pin_secret(array $cfg): string { $s = (string) ($cfg['pin_cookie_secret'] ?? ''); if ($s !== '' && $s !== 'CHANGE_ME') return $s; return hash('sha256', 'pincookie|' . POOL_ID . '|' . (string) ($cfg['register_secret'] ?? '') . '|' . (string) ($cfg['access_pin'] ?? '')); } function pin_make_cookie(array $cfg): string { $ttl = (int) ($cfg['pin_ttl_days'] ?? 30); $payload = base64_encode(json_encode(['iat' => time(), 'exp' => time() + $ttl * 86400, 'v' => 1])); return $payload . '.' . hash_hmac('sha256', $payload, pin_secret($cfg)); } function pin_cookie_valid(array $cfg): bool { if (!pin_enabled($cfg)) return false; $raw = (string) ($_COOKIE[PIN_COOKIE] ?? ''); if ($raw === '' || strpos($raw, '.') === false) return false; [$payload, $sig] = explode('.', $raw, 2); if (!hash_equals(hash_hmac('sha256', $payload, pin_secret($cfg)), $sig)) return false; $d = json_decode((string) base64_decode($payload), true); return is_array($d) && ($d['exp'] ?? 0) > time(); } function pin_check(array $cfg, string $entered): bool { $real = (string) ($cfg['access_pin'] ?? ''); if ($real === '' || $entered === '') return false; if (preg_match('/^\$2[aby]\$/', $real)) return password_verify($entered, $real); // bcrypt hash return hash_equals($real, $entered); // plaintext PIN } function handle_pin(array $cfg, string $host): void { // Same-origin guard for the POST (rejects only on an explicit host mismatch). foreach (['HTTP_ORIGIN', 'HTTP_REFERER'] as $h) { if (!empty($_SERVER[$h])) { $oh = parse_url((string) $_SERVER[$h], PHP_URL_HOST); if ($oh && strtolower($oh) !== host_only($host)) { pin_page($cfg, $host, 'Request blocked.'); } break; } } if (pin_locked()) { pin_page($cfg, $host, 'Too many attempts. Please wait a few minutes and try again.'); } if (!pin_check($cfg, (string) ($_POST['pin'] ?? ''))) { pin_fail(); pin_page($cfg, $host, 'Incorrect PIN. Please try again.'); } pin_reset(); setcookie(PIN_COOKIE, pin_make_cookie($cfg), [ 'expires' => time() + (int) ($cfg['pin_ttl_days'] ?? 30) * 86400, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); log_visit($cfg, $host, 'pin-login'); header('Location: /'); http_response_code(302); exit; } // Project display name from gate-config (falls back to the hostname) — used to // label the PIN screen and visitor log so gate.php stays identical everywhere. function project_name(array $cfg, string $host): string { $name = (string) ($cfg['project']['name'] ?? ''); return $name !== '' ? $name : host_only($host); } function pin_page(array $cfg, string $host, string $msg = ''): void { $label = htmlspecialchars((string) ($cfg['pin_label'] ?? 'Access PIN'), ENT_QUOTES); $eyebrow = htmlspecialchars(project_name($cfg, $host), ENT_QUOTES); $err = $msg ? '
' . htmlspecialchars($msg, ENT_QUOTES) . '
' : ''; if (!empty($cfg['pin_first'])) { // PIN-first mode: this page IS the front door, so the staff link must go // straight to the SSO login (a "/" link would loop back here). Stash the // return target in the parent-domain cookie, same as the interstitial. $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $target = 'https://' . host_only($host) . ($path === '/_pin' ? '/' : ($_SERVER['REQUEST_URI'] ?? '/')); setcookie('pp_return', $target, [ 'expires' => time() + 600, 'path' => '/', 'domain' => '.hntbprojectportal.com', 'secure' => true, 'httponly' => false, 'samesite' => 'Lax', ]); $alt = '
HNTB staff? Sign in with your work email.
'; } else { $alt = '
HNTB staff: sign in with your work email instead.
'; } header('Cache-Control: no-store'); header('Content-Type: text/html; charset=utf-8'); http_response_code(200); echo << Enter access PIN
$eyebrow

Enter access PIN

$label. Enter the PIN provided by the project team to view this dashboard.

$err
$alt
HNTB · Digital Project Delivery
HTML; exit; } // --- HNTB-only visitor log viewer (/_visitors) --- function render_visitors(array $cfg, string $host): void { $path = visitor_log_path($cfg); $lines = is_file($path) ? array_slice(file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES), -5000) : []; $rows = []; foreach ($lines as $ln) { $r = json_decode($ln, true); if (is_array($r)) $rows[] = $r; } $rows = array_reverse($rows); // newest first if (($_GET['format'] ?? '') === 'csv') { $slug = preg_replace('/[^a-z0-9-]/', '', strtolower(explode('.', host_only($host))[0])) ?: 'site'; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $slug . '-visitors.csv"'); $rdns = []; echo "timestamp,ip,forwarded_ip,reverse_dns,method,who,path,user_agent\r\n"; foreach ($rows as $r) { $ip = $r['ip'] ?? ''; if ($ip && !isset($rdns[$ip])) { $rdns[$ip] = (count($rdns) < 400) ? (@gethostbyaddr($ip) ?: '') : ''; } $c = function ($v) { return '"' . str_replace('"', '""', (string) $v) . '"'; }; echo implode(',', array_map($c, [$r['ts'] ?? '', $ip, $r['fwd'] ?? '', $rdns[$ip] ?? '', $r['method'] ?? '', $r['who'] ?? '', $r['path'] ?? '', $r['ua'] ?? ''])) . "\r\n"; } exit; } // Summary: unique IPs and their visit counts (with reverse-DNS). $ipCount = []; $ipLast = []; foreach ($rows as $r) { $ip = $r['ip'] ?? ''; if ($ip === '') continue; $ipCount[$ip] = ($ipCount[$ip] ?? 0) + 1; if (!isset($ipLast[$ip])) $ipLast[$ip] = $r['ts'] ?? ''; } arsort($ipCount); $rdns = []; $sumRows = ''; foreach ($ipCount as $ip => $n) { if (!isset($rdns[$ip])) $rdns[$ip] = (count($rdns) < 400) ? (@gethostbyaddr($ip) ?: '') : ''; $sumRows .= '' . htmlspecialchars($ip) . '' . htmlspecialchars($rdns[$ip]) . '' . (int) $n . '' . htmlspecialchars($ipLast[$ip]) . ''; } $recent = ''; foreach (array_slice($rows, 0, 500) as $r) { $ip = $r['ip'] ?? ''; $recent .= '' . htmlspecialchars($r['ts'] ?? '') . '' . htmlspecialchars($ip) . '' . htmlspecialchars($rdns[$ip] ?? '') . '' . htmlspecialchars($r['method'] ?? '') . '' . htmlspecialchars($r['who'] ?? '') . '' . htmlspecialchars($r['path'] ?? '') . ''; } $total = count($rows); $uniq = count($ipCount); $title = htmlspecialchars(project_name($cfg, $host), ENT_QUOTES); header('Cache-Control: no-store'); header('Content-Type: text/html; charset=utf-8'); echo << Visitor log

$title — visitor log

$total visits · $uniq unique IP addresses · newest first · reverse-DNS resolved on view
Download CSV

Unique visitors (by IP)

$sumRows
IP addressReverse DNS (org/ISP)VisitsFirst seen

Recent activity (latest 500)

$recent
Timestamp (UTC)IPReverse DNSAuthWho (SSO email)Page
HTML; exit; } // ================================ gate ================================ $host = strtolower($_SERVER['HTTP_HOST'] ?? ''); $reqPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; // --- TEMPORARY diagnostic: visit /?gatedebug=1 to see why auth fails. // Prints no secrets (not the token). Remove this block once it works. if (isset($_GET['gatedebug'])) { header('Content-Type: text/plain; charset=utf-8'); header('Cache-Control: no-store'); $tok = $_COOKIE[COOKIE] ?? ''; echo "host: $host\n"; echo "cookie '" . COOKIE . "' present: " . ($tok !== '' ? 'yes (len ' . strlen($tok) . ')' : 'NO') . "\n"; echo "openssl_verify available: " . (function_exists('openssl_verify') ? 'yes' : 'NO') . "\n"; echo "allow_url_fopen: " . (ini_get('allow_url_fopen') ? 'on' : 'off') . " | curl: " . (function_exists('curl_init') ? 'yes' : 'no') . "\n"; $jwks = get_jwks($JWKS_URI); echo "jwks keys fetched: " . count($jwks['keys']) . "\n"; if ($tok === '') { echo "=> redirecting because NO cookie reached PHP\n"; exit; } $parts = explode('.', $tok); echo "token parts: " . count($parts) . "\n"; if (count($parts) === 3) { $hdr = json_decode(b64url($parts[0]), true); $kid = $hdr['kid'] ?? '(none)'; $jwk = null; foreach ($jwks['keys'] as $k) if (($k['kid'] ?? '') === $kid) $jwk = $k; echo "kid: $kid | found in jwks: " . ($jwk ? 'yes' : 'NO') . "\n"; if ($jwk) { $sig = openssl_verify($parts[0] . '.' . $parts[1], b64url($parts[2]), jwk_to_pem($jwk), OPENSSL_ALGO_SHA256); echo "signature: " . ($sig === 1 ? 'VALID' : ($sig === 0 ? 'invalid' : 'error')) . "\n"; } $c = json_decode(b64url($parts[1]), true) ?: []; echo "iss match: " . ((($c['iss'] ?? '') === $ISSUER) ? 'yes' : 'no') . "\n"; echo "aud match: " . ((($c['aud'] ?? '') === CLIENT_ID) ? 'yes' : 'no') . "\n"; echo "token_use: " . ($c['token_use'] ?? '(none)') . "\n"; echo "exp - now (sec): " . ((int)($c['exp'] ?? 0) - time()) . "\n"; $em = strtolower((string) ($c['email'] ?? '')); $vv = $c['email_verified'] ?? false; echo "email: " . ($em ?: '(none)') . " | verified: " . json_encode($vv) . "\n"; echo "=> HNTB auto-access: " . ((($vv === true || $vv === 'true') && substr($em, -9) === '@hntb.com') ? 'YES' : 'no') . "\n"; echo "groups: " . json_encode($c['cognito:groups'] ?? []) . "\n"; echo "required_groups (this subdomain): " . json_encode($REQUIRED_GROUPS) . "\n"; } exit; } // Explicitly public paths bypass auth (optional). if (in_array($reqPath, $PUBLIC_PATHS, true)) serve_file($DEFAULT_DOC); // ---- Direct PIN access (alternate to Cognito SSO) ---- // Handle the PIN screen and any valid PIN cookie BEFORE the Cognito check so // partner-agency visitors never hit the portal SSO. HNTB staff paths (/_pin // itself excluded) still fall through to SSO below. if (pin_enabled($cfg)) { if ($reqPath === '/_pin') { if ($_SERVER['REQUEST_METHOD'] === 'POST') handle_pin($cfg, $host); pin_page($cfg, $host); } if (pin_cookie_valid($cfg)) { // /_visitors is HNTB-SSO-only; PIN users must not see it -> send to SSO. if ($reqPath !== '/_visitors') { log_visit($cfg, $host, 'pin'); serve_file($DEFAULT_DOC); } } } $token = $_COOKIE[COOKIE] ?? ''; if ($token === '') redirect_login(); $claims = verify_token($token, get_jwks($JWKS_URI), $ISSUER); if ($claims === null) redirect_login(); $groups = $claims['cognito:groups'] ?? []; // HNTB staff: any VERIFIED @hntb.com email reaches every subdomain, no grant // needed. email_verified is set by the pool (OTP login proves ownership), and // the token is signature-verified above, so the email claim is trustworthy. $email = strtolower((string) ($claims['email'] ?? '')); $ev = $claims['email_verified'] ?? false; $verified = ($ev === true || $ev === 'true' || $ev === 1 || $ev === '1'); $isHntb = $verified && substr($email, -9) === '@hntb.com'; $ok = $isHntb || (bool) array_intersect($groups, FULL_ACCESS) || ($REQUIRED_GROUPS && (bool) array_intersect($groups, $REQUIRED_GROUPS)); if (!$ok) deny($host); // HNTB-only visitor log viewer. if ($reqPath === '/_visitors') { if (!$isHntb) deny($host); render_visitors($cfg, $host); } if (pin_enabled($cfg)) log_visit($cfg, $host, 'sso', $email); maybe_register($cfg, $host); serve_file($DEFAULT_DOC);